package com.basker.pisces.core.meta.bean;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.stream.Stream;

import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.basker.pisces.core.helper.DataContractHelper;
import com.basker.pisces.core.meta.annotation.CollectionField;
import com.basker.pisces.core.meta.annotation.DataField;
import com.basker.pisces.core.meta.annotation.ObjectField;
import com.basker.pisces.core.meta.annotation.ReferenceField;
import com.basker.pisces.core.utils.ConvertUtils;
import com.basker.pisces.spring.SpringApplicationContext;
import com.basker.pisces.utils.ReflectionUtils;
import com.esotericsoftware.reflectasm.MethodAccess;

public class BeanFieldDescriptor {

    private static final Class<?>[] FIELD_ANNOTATION_TYPES = new Class<?>[]{DataField.class, ObjectField.class,
            ReferenceField.class, CollectionField.class};

    private static final boolean USEASM = SpringApplicationContext.getConfig().getBoolean("pisces.asm.enable", true);

    public static BeanFieldDescriptor[] getBeanFieldDescriptors(Class<?> beanClass) {
        Assert.notNull(beanClass, "paramter 'beanClass' is required");
        return Stream.of(BeanUtils.getPropertyDescriptors(beanClass)).filter(propertyDescriptor -> {
            if (propertyDescriptor.getReadMethod() == null || propertyDescriptor.getWriteMethod() == null) {
                return false;
            }

            if (propertyDescriptor.getReadMethod().getDeclaringClass().equals(Object.class)) {
                return false;
            }

            return ReflectionUtils.findField(beanClass, propertyDescriptor.getName()) != null;
        }).map(propertyDescriptor -> new BeanFieldDescriptor(beanClass, propertyDescriptor))
                .toArray(len -> new BeanFieldDescriptor[len]);
    }

    private Class<?> beanClass;
    private String name;
    private String propertyName;

    //	private FieldAccess fieldAccess;
//	private int fieldIndex;
    private Field field;

    private MethodAccess methodAccess;
    private int setMethodIndex;
    private Method setMethod;
    private int getMethodIndex;
    private Method getMethod;

    private Annotation fieldAnnotation;
    private boolean isCollectionType = false;

    public BeanFieldDescriptor(Class<?> beanClass, PropertyDescriptor propertyDescriptor) {
        this.beanClass = beanClass;

        this.propertyName = propertyDescriptor.getName();
        this.name = StringUtils.capitalize(propertyName);

        this.getMethod = propertyDescriptor.getReadMethod();
        this.setMethod = propertyDescriptor.getWriteMethod();

        if (USEASM) {
            this.methodAccess = ReflectionUtils.getAsmMethodAccess(this.beanClass);
            this.getMethodIndex = this.methodAccess.getIndex(this.getMethod.getName(), 0);
            this.setMethodIndex = this.methodAccess.getIndex(this.setMethod.getName(), 1);
        }

        this.field = ReflectionUtils.findField(beanClass, propertyName);
        this.field.setAccessible(true);
//		this.fieldAccess = FieldAccess.get(this.beanClass);
//		this.fieldIndex = this.fieldAccess.getIndex(this.field.getName());

        this.fieldAnnotation = this.findFieldAnnotation();
        if (this.fieldAnnotation != null) {
            Object value = AnnotationUtils.getValue(this.fieldAnnotation, "name");
            if (value != null && !"".equals(value)) {
                this.name = String.valueOf(value);
            }
        }

        this.isCollectionType = Collection.class.isAssignableFrom(this.getProperyType());
    }

    public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
        T annotation = AnnotationUtils.findAnnotation(this.field, annotationClass);

        if (annotation == null) {
            annotation = AnnotationUtils.findAnnotation(this.getMethod, annotationClass);
        }

        return annotation;
    }

    public Class<?> getBeanClass() {
        return beanClass;
    }

    /**
     * 如果字段是集合类型，尝试从泛型获取子项的类型
     *
     * @return
     */
    public Class<?> getCollectionItemType() {
        if (!this.isCollectionType()) {
            return null;
        }

        ResolvableType methodReturnType = ResolvableType.forMethodReturnType(this.getGetMethod());
        return methodReturnType.getGeneric(0).resolve();
    }

    public Field getField() {
        return field;
    }

    public Annotation getFieldAnnotation() {
        return fieldAnnotation;
    }

    public Method getGetMethod() {
        return getMethod;
    }

    public String getName() {
        return name;
    }

    public String getPropertyName() {
        return propertyName;
    }

    public Class<?> getProperyType() {
        return this.field.getType();
    }

    public Method getSetMethod() {
        return setMethod;
    }

    public Object getValue(Object dataObject) {
//		this.checkDataObjectType(dataObject);

        return USEASM ? this.getValueByAsmRef(dataObject) : this.getValueByJdkRef(dataObject);
    }

    public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return this.getAnnotation(annotationClass) != null;
    }

    /**
     * 当前字段是否是集合类型
     *
     * @return
     */
    public boolean isCollectionType() {
        return this.isCollectionType;
    }

    public boolean isFieldAnnotationPresent() {
        return this.fieldAnnotation != null;
    }

    public void setValue(Object dataObject, Object value) {
        this.setValue(dataObject, value, false);
    }

    public void setValue(Object dataObject, Object value, boolean resolveValue) {
        Object resovledValue = resolveValue ? ConvertUtils.convert(value, this.getProperyType()) : value;

//		this.checkDataObjectType(dataObject);

        if (USEASM) {
            this.setValueByAsmRef(dataObject, resovledValue);
        } else {
            this.setValueByJdkRef(dataObject, resovledValue);
        }
    }

    @Override
    public String toString() {
        return this.getName();
    }

//	private void checkDataObjectType(Object dataObject) {
    // 工具测试发现，这步会成为热点，所以不执行

    // Assert.notNull(dataObject, "parameter 'dataObject' is required!");
    //
    // if (!this.beanClass.isAssignableFrom(dataObject.getClass())) {
    // throw new IllegalArgumentException(
    // "parameter 'dataObject' should be a " + this.beanClass.getSimpleName() + "
    // type");
    // }
//	}

    private Annotation findFieldAnnotation() {
        for (Class<?> type : FIELD_ANNOTATION_TYPES) {
            @SuppressWarnings("unchecked")
            Class<? extends Annotation> annotationType = (Class<? extends Annotation>) type;

            Annotation ret = this.getAnnotation(annotationType);
            if (ret != null) {
                return ret;
            }
        }

        return null;
    }

    private Object getValueByAsmRef(Object dataObject) {
//		if (DataContractHelper.isInInitial(dataObject)) {
//			return this.fieldAccess.get(dataObject, this.fieldIndex);
//		}

        return this.methodAccess.invoke(dataObject, this.getMethodIndex);
    }

    private Object getValueByJdkRef(Object dataObject) {
        if (DataContractHelper.isInInitial(dataObject)) {
            return ReflectionUtils.getFieldValue(this.field, dataObject);
        }

        return ReflectionUtils.invokeMethod(this.getMethod, dataObject);
    }

    private void setValueByAsmRef(Object dataObject, Object value) {
//		if (DataContractHelper.isInInitial(dataObject)) {
//			this.fieldAccess.set(dataObject, this.fieldIndex, value);
//		} else {
        this.methodAccess.invoke(dataObject, this.setMethodIndex, value);
//		}
    }

    private void setValueByJdkRef(Object dataObject, Object value) {
        if (DataContractHelper.isInInitial(dataObject)) {
            ReflectionUtils.setFieldValue(this.field, dataObject, value);
        } else {
            ReflectionUtils.invokeMethod(this.setMethod, dataObject, value);
        }
    }
}
